package comp

import (
	"archive/tar"
	"compress/gzip"
	"io"
	"log"
	"os"
	"strings"
)

// 默认分隔符，用来区source和dest两个目录中对于文件是"新增" "更新" "删除" "不变"
const sep = ","

// Diff _
func Diff(source, dest, tarDest string) error {
	sourceFile := new(CFile)
	sourceFile.Parse(source, source)

	destFile := new(CFile)
	destFile.Parse(dest, dest)

	merge := mergePaths(sourceFile.DictPaths, destFile.DictPaths)

	basePath := destFile.Path
	// 如果对比的是两个压缩包文件，那么要设置为解压缩的临时目录
	if destFile.IsCompressed() {
		basePath = destFile.DeCompressDir
	}

	todoFiles := diffAndGenerate2CompressedFiles(merge, basePath)

	err := zipDiffFiles(tarDest, todoFiles)
	if err != nil {
		return err
	}

	// 删掉临时解压缩目录的文件
	if sourceFile.IsCompressed() {
		os.RemoveAll(sourceFile.DeCompressDir)
	}

	if destFile.IsCompressed() {
		os.RemoveAll(destFile.DeCompressDir)
	}

	return nil
}

// mergePaths 将 source paths 和 dest paths 的map根据下面规则合并，用于区分 新增 更新 删除，
// 方便后续进行增量文件打包压缩
func mergePaths(sourcePaths, destPaths map[string]string) map[string]string {
	merge := map[string]string{}

	// source 目录文件资源路径map，默认都是待删除状态
	for path, md5 := range sourcePaths {
		merge[path] = md5 + sep
	}

	/**
	 * dest 目录文件资源路径map，循环判断source中是否存在该文件路径；
	 * 如果不存在，则对于文件资源是新增，表示为：",dest-md5"
	 * 如果存在，则要判断source和dest中文件的md5值是否相等:
	 *     如果相等，则将source的md5去掉sep，表示为："source-md5"
	 *     如果不相等，则将source的md5和dest的md5根据sep拼接到一起，表示为："source-md5,dest-md5"
	 */
	for path, md5 := range destPaths {
		sourceSep, ok := merge[path]
		// add
		if !ok {
			merge[path] = sep + md5
			continue
		}

		if sourceSep == (md5 + sep) { // equal
			merge[path] = sourceSep[0 : len(sourceSep)-1]
		} else { // update
			merge[path] = sourceSep + md5
		}
	}

	return merge
}

func diffAndGenerate2CompressedFiles(paths map[string]string, basePath string) map[string]string {
	todoFiles := map[string]string{}
	/**
	 * 遍历merge后的map，根据判断value值(MD5)的sep分隔符位置进行对应处理，包括：
	 *		相等：md5不包含sep，不做任何处理
	 *		删除：md5最后一个字符是sep，写入到待删除的txt文件，并记录要删除的文件的相对路径
	 *		新增/更新：md5中包含sep，并且位置不在最后一位(排查删除)，则根据dest目录的决定路径+变更文件的相对路径，并记录
	 * 				  如果进行增量对比的是压缩文件，则要将解压缩的临时绝对目录+变更文件的相对路径
	 */
	for filename, md5 := range paths {
		switch strings.Index(md5, sep) {
		case -1: // equal

		case len(md5) - 1: // delete
			// TODO

		default: // add, update
			tmpFilename := getAbsPath(basePath, filename)
			todoFiles[tmpFilename] = filename
		}
	}

	return todoFiles
}

func zipDiffFiles(output string, files map[string]string) error {
	gzFile, err := os.Create(output)
	if err != nil {
		log.Println("create dest file error, err:", err)
		return err
	}
	defer gzFile.Close()

	gw := gzip.NewWriter(gzFile)
	defer gw.Close()

	tw := tar.NewWriter(gw)
	for fullName, fileName := range files {
		err := compress(fullName, fileName, tw)
		if err != nil {
			log.Println(err)
			return err
		}
	}

	return nil
}

func compress(fullName string, fileName string, tw *tar.Writer) error {
	file, err := os.Open(fullName)
	if err != nil {
		log.Println("compress open file error, err:", err)
		return err
	}
	defer file.Close()

	info, err := file.Stat()
	if err != nil {
		log.Println("compress file stat error, err:", err)
		return err
	}

	// 信息头
	h := new(tar.Header)
	h.Name = fileName
	h.Size = info.Size()
	h.Mode = int64(info.Mode())
	h.ModTime = info.ModTime()

	err = tw.WriteHeader(h)
	if err != nil {
		log.Println("compress write header error, err:", err)
		return err
	}

	_, err = io.Copy(tw, file)
	if err != nil {
		log.Println("compress io copy file error, err:", err)
		return err
	}

	return nil
}
